<?php

class Skjb_Audio_Mp3
{
    const MPEG_VERSION_1_0 = '11';
    const MPEG_VERSION_2_0 = '10';
    const MPEG_VERSION_2_5 = '00';
    
    const LAYER_DESCRIPTION_1 = '11';
    const LAYER_DESCRIPTION_2 = '10';
    const LAYER_DESCRIPTION_3 = '01';
    
    private static $_bitrates = array(
        '0000' => array('free','free','free','free','free'),
        '0001' => array(  '32',  '32',  '32',  '32',   '8'),
        '0010' => array(  '64',  '48',  '40',  '48',  '16'),
        '0011' => array(  '96',  '56',  '48',  '56',  '24'),
        '0100' => array( '128',  '64',  '56',  '64',  '32'),
        '0101' => array( '160',  '80',  '64',  '80',  '40'),
        '0110' => array( '192',  '96',  '80',  '96',  '48'),
        '0111' => array( '224', '112',  '96', '112',  '56'),
        '1000' => array( '256', '128', '112', '128',  '64'),
        '1001' => array( '288', '160', '128', '144',  '80'),
        '1010' => array( '320', '192', '160', '160',  '96'),
        '1011' => array( '352', '224', '192', '176', '112'),
        '1100' => array( '384', '256', '224', '192', '128'),
        '1101' => array( '416', '320', '256', '224', '144'),
        '1110' => array( '448', '384', '320', '256', '160'),
        '1111' => array( 'bad', 'bad', 'bad', 'bad', 'bad'),
    );
    
    private static $_frequencies = array(
        '00' => array('44100', '22050', '11025'),
        '01' => array('48000', '24000', '12000'),
        '10' => array('32000', '16000',  '8000'),
        '11' => array(  'res',   'res',   'res'),
    );

    private $_path = null;
    
    private $_fileSize = null;
    
    private $_headerSize = 10;
    private $_footerSize = 10;
    
    private $_tagSize = null;
    
    private $_firstSyncOffset = null;
    
    private $_mpegVersion = null;
    private $_layerDescription = null;
    private $_bitrateIndex = null;
    private $_samplingFrequencyIndex = null;
    private $_paddingBit = null;
    private $_footerPresent = null;
    
    private $_filePointer = null;
    
    public function __construct($path)
    {
        $this->_path = $path;
        
        // Check for ID3
        $bytes = $this->_readBytes(0, 3);
        $id3 = chr($bytes[1]) . chr($bytes[2]) . chr($bytes[3]);
        
        if ($id3 != 'ID3') {
            throw new Exception('Unsupported MP3 format');
        }
        
        $this->_getFirstSyncOffset();
        
        // Get ID3 Version
        //$inBlocks = fread($this->_fp, 2);
        //$blocks = unpack('C*', $inBlocks);
        //echo 'ID3v2 Version: ' . $blocks[1] . '.' . $blocks[2] . '<br />';
        
        //echo 'Unsynchronisation: ' . $this->_getBit($flags, 0) . '<br />';
        //echo 'Extended Header: ' . $this->_getBit($flags, 1) . '<br />';
        //echo 'Experimental Indicator: ' . $this->_getBit($flags, 2) . '<br />';
        
        
        /*$inBlocks = fread($this->_fp, 3);
        $blocks = unpack('C*', $inBlocks);
        
        echo 'Protection Bit: ' . $this->_getBit($blocks[1], 7) . '<br />';
        echo 'Private Bit: ' . $this->_getBit($blocks[2], 7) . '<br />';
        echo 'Channel Mode: ' . $this->_getBit($blocks[3], 0) . $this->_getBit($blocks[3], 1) . '<br />';
        echo 'Mode Extension: ' . $this->_getBit($blocks[3], 2) . $this->_getBit($blocks[3], 3) . '<br />';
        echo 'Copyright: ' . $this->_getBit($blocks[3], 4) . '<br />';
        echo 'Original Media: ' . $this->_getBit($blocks[3], 5) . '<br />';
        echo 'Emphasis: ' . $this->_getBit($blocks[3], 6) . '<br />';*/
    }
    
    private function _getFirstSyncOffset()
    {
        if ($this->_firstSyncOffset === null) {
            fseek($this->_getFilePointer(), $this->getTagSize() + $this->_headerSize + ($this->getFooterPresent() == 1 ? $this->_footerSize : 0)); // Fast forward past the header and tags
            
            // The largest frame size is 4096 bytes (256kbps@8000Hz)
            // We want to look for a frame sync (0xFF) within the first 4096 bytes of data
            // to make sure it's a CBR file
            $foundSync = false;
            for ($i = 0; $i < 5000; $i++) {
                $inBlocks = fread($this->_getFilePointer(), 1);
                $blocks = unpack('C*', $inBlocks);
                
                if ($blocks[1] == 255) {
                    $this->_firstSyncOffset = $this->getTagSize() + $this->_headerSize + ($this->getFooterPresent() == 1 ? $this->_footerSize : 0) + $i;
                    $foundSync = true;
                    break;
                }
            }
            
            if (!$foundSync) {
                throw new Exception('VBR files are not supported');
            }
        }
        
        return $this->_firstSyncOffset;
    }
    
    public function getMpegVersion()
    {
        if ($this->_mpegVersion === null) {
            $byte = $this->_readByte($this->_getFirstSyncOffset() + 1);
            $this->_mpegVersion = $this->_getBits($byte, 3, 2);
        }
        
        return $this->_mpegVersion;
    }
    
    public function getMpegVersionName()
    {
        switch ($this->getMpegVersion()) {
            case self::MPEG_VERSION_1_0:
            	return 'MPEG-1';
        	case self::MPEG_VERSION_2_0:
            	return 'MPEG-2';
        	case self::MPEG_VERSION_2_5:
            	return 'MPEG-2.5';
        }
    }
    
    public function getLayerDescription()
    {
        if ($this->_layerDescription === null) {
            $byte = $this->_readByte($this->_getFirstSyncOffset() + 1);
            $this->_layerDescription = $this->_getBits($byte, 5, 2);
        }
        
        return $this->_layerDescription;
    }
    
    public function getLayerDescriptionName()
    {
        switch ($this->getLayerDescription()) {
            case self::LAYER_DESCRIPTION_1:
            	return 'Layer 1';
            case self::LAYER_DESCRIPTION_2:
            	return 'Layer 2';
            case self::LAYER_DESCRIPTION_3:
            	return 'Layer 3';
        }
    }
    
    public function getBitrateIndex()
    {
        if ($this->_bitrateIndex === null) {
            $byte = $this->_readByte($this->_getFirstSyncOffset() + 2);
            $this->_bitrateIndex = $this->_getBits($byte, 0, 4);
        }
        
        return $this->_bitrateIndex;
    }
    
    public function getBitrate()
    {
        switch ($this->getMpegVersion()) {
            case self::MPEG_VERSION_1_0:
            	switch ($this->getLayerDescription()) {
            	    case self::LAYER_DESCRIPTION_1:
                    	return self::$_bitrates[$this->getBitrateIndex()][0];
                    case self::LAYER_DESCRIPTION_2:
                    	return self::$_bitrates[$this->getBitrateIndex()][1];
                    case self::LAYER_DESCRIPTION_3:
                    	return self::$_bitrates[$this->getBitrateIndex()][2];
            	}
            	break;
            case self::MPEG_VERSION_2_0:
            case self::MPEG_VERSION_2_5:
            	switch ($this->getLayerDescription()) {
            	    case self::LAYER_DESCRIPTION_1:
                    	return self::$_bitrates[$this->getBitrateIndex()][3];
                    case self::LAYER_DESCRIPTION_2:
                    case self::LAYER_DESCRIPTION_3:
                    	return self::$_bitrates[$this->getBitrateIndex()][4];
            	}
            	break;
        }
    }
    
    public function getSamplingFrequencyIndex()
    {
        if ($this->_samplingFrequencyIndex === null) {
            $byte = $this->_readByte($this->_getFirstSyncOffset() + 2);
            $this->_samplingFrequencyIndex = $this->_getBits($byte, 4, 2);
        }
        
        return $this->_samplingFrequencyIndex;
    }
    
    public function getSamplingFrequency()
    {
        switch ($this->getMpegVersion()) {
            case self::MPEG_VERSION_1_0:
            	return self::$_frequencies[$this->getSamplingFrequencyIndex()][0];
        	case self::MPEG_VERSION_2_0:
            	return self::$_frequencies[$this->getSamplingFrequencyIndex()][1];
        	case self::MPEG_VERSION_2_5:
            	return self::$_frequencies[$this->getSamplingFrequencyIndex()][2];
        }
    }
    
    public function getPaddingBit()
    {
        if ($this->_paddingBit === null) {
            $byte = $this->_readByte($this->_getFirstSyncOffset() + 2);
            $this->_paddingBit = $this->_getBit($byte, 6);
        }
        
        return $this->_paddingBit;
    }
    
    public function getFrameSize()
    {
        return ceil((144 * $this->getBitrate() * 1000) / $this->getSamplingFrequency()) + $this->getPaddingBit();
    }
    
    public function getFilesize()
    {
        if ($this->_fileSize === null) {
            $this->_fileSize = filesize($this->_path);
        }
        
        return $this->_fileSize;
    }
    
    public function getSeconds()
    {
        $kilobytesPerSecond = ($this->getBitrate() * 1000) / 8; // I'm surprised this wasn't 1024
        $datasize = $this->getFilesize() - ($this->_getFirstSyncOffset() + 4 / 8);
        $length = $datasize / $kilobytesPerSecond;
        
        return round($length);
    }
    
    public function getDuration()
    {
        $seconds = $this->getSeconds();
        $minutes = floor($seconds / 60);
        $hours = floor($minutes / 60);
        $minutes -= $hours * 60;
        $seconds -= ($hours * 60 * 60) + ($minutes * 60);
        
        return array(
            'hours'   => $hours,
            'minutes' => $minutes,
            'seconds' => $seconds,
        );
    }
    
    public function getFooterPresent()
    {
        if ($this->_footerPresent === null) {
            $byte = $this->_readByte(5);
            $this->_footerPresent = $this->_getBit($byte, 3);
        }
        
        return $this->_footerPresent;
    }
    
    public function getTagSize()
    {
        if ($this->_tagSize === null) {
            $blocks = $this->_readBytes(6, 4);
            $binTagSize = '';
            foreach ($blocks as $block) {
                // For some reason we don't want the first bit
                $binTagSize .= $this->_getBits($block, 1, 7);
            }
            $this->_tagSize = bindec($binTagSize);
        }
        
        return $this->_tagSize;
    }
    
    private function _getFilePointer()
    {
        if ($this->_filePointer === null) {
            $this->_filePointer = fopen($this->_path, 'rb');
        }
        
        return $this->_filePointer;
    }
    
    private function _readByte($offset)
    {
        $bytes = $this->_readBytes($offset, 1);
        
        return $bytes[1];
    }
    
    private function _readBytes($offset, $length)
    {
        fseek($this->_getFilePointer(), $offset);
        $inBlocks = fread($this->_getFilePointer(), $length);
        $blocks = unpack('C*', $inBlocks);
        
        return $blocks;
    }
    
    private function _getBits($byte, $offset, $count)
    {
        $result = '';
        
        for ($i = 0; $i < $count; $i++) {
            $result .= $this->_getBit($byte, $offset + $i);
        }
        
        return $result;
    }
    
    private function _getBit($byte, $bitNum)
    {
        $pow = array(
            0 => 1,
            1 => 2,
            2 => 4,
            3 => 8,
            4 => 16,
            5 => 32,
            6 => 64,
            7 => 128,
        );
        
        $whichbit = 7 - $bitNum; // Work out the index
        $mult = $pow[$whichbit]; // Is essentially 2 to the power of $whichbit
        $b = $byte & $mult; // Combines the bits of $byte and $mult
        $b = $b >> $whichbit; // Divides $b by 2 to the power of $whichbit
        
        return $b;
    }
}